<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XML工具 (排序、压缩、格式化)</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background-color: #f0f2f5;
            padding: 20px;
            color: #333;
        }
        .container {
            background-color: white;
            padding: 2rem;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
            width: 95%;
            max-width: 1400px;
            display: flex;
            flex-direction: column;
            height: 90vh;
        }
        h1 {
            text-align: center;
            margin-top: 0;
            color: #2c3e50;
            margin-bottom: 1.5rem;
            font-size: 1.8rem;
        }
        .main-content {
            display: flex;
            gap: 2rem;
            flex: 1;
            min-height: 0;
        }
        @media (max-width: 768px) {
            .main-content {
                flex-direction: column;
            }
        }
        .panel {
            flex: 1;
            display: flex;
            flex-direction: column;
            border: 1px solid #e1e5eb;
            border-radius: 8px;
            overflow: hidden;
            background-color: #ffffff;
            box-shadow: 0 2px 5px rgba(0,0,0,0.05);
        }
        .panel-header {
            background-color: #4a6fa5;
            color: white;
            padding: 1rem;
            text-align: center;
            font-weight: bold;
            font-size: 1.1rem;
        }
        .panel.input .panel-header { background-color: #28a745; }
        .panel.output .panel-header { background-color: #17a2b8; }
        
        .input-area, .output-area {
            display: flex;
            flex-direction: column;
            flex: 1;
            padding: 1rem;
        }
        label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: 600;
            color: #495057;
        }
        textarea, .result {
            flex: 1;
            width: 100%;
            padding: 0.75rem;
            border: 1px solid #ced4da;
            border-radius: 4px;
            box-sizing: border-box;
            font-family: 'Consolas', 'Courier New', monospace;
            resize: none;
            margin-bottom: 1rem;
            background-color: #fff;
        }
        textarea:focus, .result:focus {
             outline: none;
             border-color: #4a6fa5;
             box-shadow: 0 0 0 3px rgba(74, 111, 165, 0.25);
        }
        .result {
            background-color: #f8f9fa;
            overflow: auto;
            white-space: pre;
        }
        .config-area {
             padding: 0 1rem 1rem 1rem;
             background-color: #f8f9fa;
             border-top: 1px solid #e1e5eb;
        }
        .form-group {
            display: flex;
            flex-wrap: wrap;
            gap: 1rem;
            margin-bottom: 0.75rem;
            align-items: center;
        }
        .form-group label {
            min-width: 100px;
            margin-bottom: 0;
            display: flex;
            align-items: center;
        }
        .form-group input, .form-group select {
            flex: 1;
            padding: 0.5rem;
            border: 1px solid #ced4da;
            border-radius: 4px;
        }
        .button-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            padding: 0 1rem 1rem 1rem;
        }
        button {
            padding: 0.6rem 1.2rem;
            background-color: #4a6fa5;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            flex: 1;
            min-width: 100px;
            font-size: 0.9rem;
            font-weight: 600;
            transition: background-color 0.2s, transform 0.1s;
        }
        button:hover {
            background-color: #3a5a80;
            transform: translateY(-2px);
        }
        button:active {
            transform: translateY(0);
        }
        .panel.input button.format { background-color: #28a745; }
        .panel.input button.format:hover { background-color: #218838; }
        
        .panel.input button.compress { background-color: #ffc107; color: black; }
        .panel.input button.compress:hover { background-color: #e0a800; }
        
        .panel.input button.sort { background-color: #6f42c1; }
        .panel.input button.sort:hover { background-color: #5a32a3; }

        .panel.output button.copy-btn { background-color: #6c757d; }
        .panel.output button.copy-btn:hover { background-color: #5a6268; }
        
        .panel.output button.download-btn { background-color: #17a2b8; }
        .panel.output button.download-btn:hover { background-color: #138496; }

        .error {
            color: #dc3545;
            font-size: 0.85rem;
            margin-top: 0.25rem;
            min-height: 1.2rem;
            padding: 0 1rem;
        }
        .hint {
            font-size: 0.8rem;
            color: #6c757d;
            margin-top: 0.25rem;
            text-align: center;
            padding: 0 1rem 1rem 1rem;
        }
        .status {
            text-align: center;
            padding: 0.5rem;
            font-weight: bold;
        }
        .status.success { color: #28a745; }
        .status.error { color: #dc3545; }
    </style>
</head>
<body>

<div class="container">
    <h1>XML工具 (排序、压缩、格式化)</h1>
    <div class="main-content">
        <!-- XML输入与操作面板 -->
        <div class="panel input">
            <div class="panel-header">XML输入与操作</div>
            <div class="input-area">
                <label for="xmlInput">输入XML:</label>
                <textarea id="xmlInput" placeholder="粘贴您的XML数据..."><note>
    <to>Tove</to>
    <from>Jani</from>
    <heading>Reminder</heading>
    <body>Don't forget me this weekend!</body>
</note></textarea>
                <div class="error" id="xmlError"></div>
            </div>
            
            <div class="config-area">
                <div class="form-group">
                    <label for="indentSize">格式化缩进:</label>
                    <select id="indentSize">
                        <option value="2">2个空格</option>
                        <option value="4" selected>4个空格</option>
                        <option value="tab">Tab</option>
                    </select>
                </div>
            </div>
            
            <div class="button-group">
                <button id="formatBtn" class="format">格式化</button>
                <button id="compressBtn" class="compress">压缩</button>
                <button id="sortBtn" class="sort">排序</button>
                <button id="copyInputBtn">复制输入</button>
                <button id="clearInputBtn">清空输入</button>
            </div>
            <div class="hint">支持格式化、压缩和按标签名排序XML节点。</div>
        </div>

        <!-- XML输出面板 -->
        <div class="panel output">
            <div class="panel-header">处理结果</div>
            <div class="output-area">
                <label for="xmlResult">输出XML:</label>
                <pre id="xmlResult" class="result" readonly></pre>
                 <div class="status" id="operationStatus"></div>
            </div>
            <div class="button-group">
                <button id="copyOutputBtn" class="copy-btn">复制结果</button>
                <button id="downloadOutputBtn" class="download-btn">下载文件</button>
                <button id="clearOutputBtn">清空结果</button>
            </div>
            <div class="hint">结果会根据所选操作实时更新。</div>
        </div>
    </div>
</div>

<script>
    // DOM 元素
    const xmlInput = document.getElementById('xmlInput');
    const xmlError = document.getElementById('xmlError');
    const indentSizeSelect = document.getElementById('indentSize');
    const formatBtn = document.getElementById('formatBtn');
    const compressBtn = document.getElementById('compressBtn');
    const sortBtn = document.getElementById('sortBtn');
    const copyInputBtn = document.getElementById('copyInputBtn');
    const clearInputBtn = document.getElementById('clearInputBtn');

    const xmlResult = document.getElementById('xmlResult');
    const operationStatus = document.getElementById('operationStatus');
    const copyOutputBtn = document.getElementById('copyOutputBtn');
    const downloadOutputBtn = document.getElementById('downloadOutputBtn');
    const clearOutputBtn = document.getElementById('clearOutputBtn');

    // 确定缩进字符串
    function getIndentString() {
        const type = indentSizeSelect.value;
        if (type === 'tab') return '\t';
        return ' '.repeat(parseInt(type));
    }

    // 解析XML字符串为DOM对象
    function parseXmlString(xmlString) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(xmlString, "application/xml");
        const errorNode = doc.querySelector("parsererror");
        if (errorNode) {
            throw new Error(`XML解析错误: ${errorNode.textContent || '无效的XML'}`);
        }
        return doc;
    }

    // 将DOM对象序列化为字符串
    function serializeXmlDoc(doc) {
        const serializer = new XMLSerializer();
        return serializer.serializeToString(doc);
    }

    // 格式化XML (使用浏览器内置功能)
    function formatXml(xmlString) {
        try {
            const doc = parseXmlString(xmlString);
            const indent = getIndentString();
            
            // 为了格式化，我们需要递归遍历并添加换行和缩进
            // 一个简单但有效的方法是使用XMLSerializer，然后用正则表达式进行格式化
            // 但更精确的方法是手动遍历DOM
            
            // 这里采用一种混合方法：先序列化，然后尝试美化
            // 注意：浏览器的XMLSerializer可能不会自动添加换行和缩进
            // 因此我们需要手动实现
            
            const formatted = formatXmlManual(doc.documentElement, indent, 0);
            // 添加XML声明
            return '<?xml version="1.0" encoding="UTF-8"?>\n' + formatted;
        } catch (e) {
            throw new Error(`格式化失败: ${e.message}`);
        }
    }
    
    // 手动格式化XML节点
    function formatXmlManual(node, indent, level) {
        let formatted = '';
        const indentation = indent.repeat(level);
        
        if (node.nodeType === Node.TEXT_NODE) {
            const textContent = node.textContent.trim();
            if (textContent) {
                formatted += indentation + textContent + '\n';
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            const nodeName = node.nodeName;
            formatted += indentation + '<' + nodeName;
            
            // 添加属性
            const attributes = node.attributes;
            for (let i = 0; i < attributes.length; i++) {
                formatted += ' ' + attributes[i].nodeName + '="' + attributes[i].nodeValue + '"';
            }
            
            // 处理子节点
            const children = node.childNodes;
            if (children.length === 0) {
                formatted += ' />\n';
            } else {
                formatted += '>\n';
                for (let i = 0; i < children.length; i++) {
                    formatted += formatXmlManual(children[i], indent, level + 1);
                }
                formatted += indentation + '</' + nodeName + '>\n';
            }
        }
        // 忽略注释等其他节点类型以简化
        
        return formatted;
    }


    // 压缩XML (移除空白字符)
    function compressXml(xmlString) {
        try {
            // 简单的压缩：移除多余的空白，但保留节点之间的必要空格
            // 更好的方法是解析后重新序列化，不添加额外空格
            const doc = parseXmlString(xmlString);
            // 使用XMLSerializer，默认情况下它不会添加额外的空白
            let compressed = serializeXmlDoc(doc);
            // 移除可能由格式化引入的换行和多余空格 (简单处理)
            compressed = compressed.replace(/>\s+</g, '><');
            return compressed;
        } catch (e) {
            throw new Error(`压缩失败: ${e.message}`);
        }
    }

    // 排序XML子节点 (按标签名)
    function sortXml(xmlString) {
        try {
            const doc = parseXmlString(xmlString);
            sortXmlNodes(doc.documentElement);
            return serializeXmlDoc(doc);
        } catch (e) {
            throw new Error(`排序失败: ${e.message}`);
        }
    }
    
    // 递归排序节点的子元素
    function sortXmlNodes(node) {
        const children = node.childNodes;
        // 将子节点转换为数组并过滤出元素节点
        const elementChildren = Array.from(children).filter(child => child.nodeType === Node.ELEMENT_NODE);
        
        if (elementChildren.length > 0) {
            // 按标签名排序
            elementChildren.sort((a, b) => a.nodeName.localeCompare(b.nodeName));
            
            // 从父节点中移除所有元素子节点
            elementChildren.forEach(child => node.removeChild(child));
            
            // 按排序后的顺序重新添加
            elementChildren.forEach(child => node.appendChild(child));
            
            // 递归排序每个子元素的子节点
            elementChildren.forEach(child => sortXmlNodes(child));
        }
    }


    // 执行操作的主函数
    async function performOperation(operation) {
        const xmlStr = xmlInput.value.trim();
        xmlError.textContent = '';
        operationStatus.textContent = '';
        operationStatus.className = 'status';
        xmlResult.textContent = '';

        if (!xmlStr) {
            xmlError.textContent = '请输入XML数据';
            return;
        }

        try {
            let resultXml = '';
            switch (operation) {
                case 'format':
                    resultXml = formatXml(xmlStr);
                    operationStatus.textContent = 'XML格式化成功!';
                    operationStatus.className = 'status success';
                    break;
                case 'compress':
                    resultXml = compressXml(xmlStr);
                    operationStatus.textContent = 'XML压缩成功!';
                    operationStatus.className = 'status success';
                    break;
                case 'sort':
                    resultXml = sortXml(xmlStr);
                    operationStatus.textContent = 'XML节点排序成功!';
                    operationStatus.className = 'status success';
                    break;
                default:
                    throw new Error('未知操作');
            }
            xmlResult.textContent = resultXml;
        } catch (e) {
            console.error(`执行${operation}操作时出错:`, e);
            xmlError.textContent = e.message;
            operationStatus.textContent = '操作失败';
            operationStatus.className = 'status error';
        }
    }


    // 复制到剪贴板
    async function copyToClipboard(text, button, successMessage = "已复制!") {
        if (!text) return;
        try {
            await navigator.clipboard.writeText(text);
            const originalText = button.textContent;
            button.textContent = successMessage;
            setTimeout(() => { button.textContent = originalText; }, 2000);
        } catch (err) {
            console.error('复制失败: ', err);
            // 可以添加更友好的用户提示
        }
    }

    // 下载文件
    function downloadFile(filename, content) {
        const blob = new Blob([content], { type: 'application/xml;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }


    // 事件监听
    formatBtn.addEventListener('click', () => performOperation('format'));
    compressBtn.addEventListener('click', () => performOperation('compress'));
    sortBtn.addEventListener('click', () => performOperation('sort'));
    
    copyInputBtn.addEventListener('click', () => copyToClipboard(xmlInput.value, copyInputBtn));
    copyOutputBtn.addEventListener('click', () => copyToClipboard(xmlResult.textContent, copyOutputBtn, "结果已复制!"));
    
    downloadOutputBtn.addEventListener('click', () => {
        const code = xmlResult.textContent;
        if (code) {
            downloadFile(`output.xml`, code);
        }
    });

    clearInputBtn.addEventListener('click', () => {
        xmlInput.value = '';
        xmlError.textContent = '';
    });
    clearOutputBtn.addEventListener('click', () => {
        xmlResult.textContent = '';
        operationStatus.textContent = '';
        operationStatus.className = 'status';
    });

    // 初始化示例 (加载页面时格式化示例XML)
    window.addEventListener('load', () => {
        performOperation('format');
    });

</script>

</body>
</html>
